package mazeMultiThread;

import java.util.ArrayList;
import java.util.Random;


/**
 * Maze generator that can run in a parallel thread to the rendering process
 * @author Dani
 *
 */
public class Maze extends Thread{
	//width and height in NODES
	private int width;
	private int height;
	//boolean map
	private boolean[][] map;
	//random number generator to be used by the algorithm
	private Random rnd;
	//array for the stack of nodes to be visited
	ArrayList<Point> points = new ArrayList<Point>();
	
	//kills the generation ending the current thread
	private boolean KILL = false;

	//various variables used for displaying information
	private int placeStackStatus = 0;
	private int maxStackSize = 0;
	private int placedSquaresStatus = 0;
	private int colisionesStatus = 0;
	private long startTimeGen = 0;
	private boolean generating = false;
	
	/**
	 * Builds the new map for the maze and sets the current width and height
	 * @param width
	 * @param height
	 */
	public Maze(int width, int height) {
		this.width = width;
		this.height = height;
		this.map = new boolean[width*2-1][height*2-1];
	}
	
	/**
	 * Generates a maze with more than one pathway (potentially) in between nodes
	 * @param seed seed to generate the maze with
	 * @param whiteNoise 1 out of whiteNoise walls will be removed
	 */
	public void generateMultipleChoice(long seed, int whiteNoise) {
		generate(seed);
		Random b = new Random();
		for (int i = 0; i < this.width*2-1; i++) {
			for (int j = 0; j < this.height*2-1;j++) {
				if ((i+j)%2 != 0)
					if (b.nextInt(whiteNoise) == 0)
						this.map[i][j] = true;
			}
		}
	}
	/**
	 * Sets all the needed parameters for a new maze generation and launches the generation process
	 * @param seed seed to generate the maze with
	 */
	public void generate(long seed) {
		//initialize the random number generator
		rnd = new Random(seed);
		//set all variables to their default values
		this.map = new boolean[width*2-1][height*2-1];
		this.placeStackStatus = 1;
		this.maxStackSize = 1;
		this.placedSquaresStatus = 1;
		this.colisionesStatus = 0;
		//time of the visual generation (substacts time paused)
		this.startTimeGen = System.currentTimeMillis();
		//actual time of generation (counting paused time)
		long time = System.currentTimeMillis();
		//set the starting generation point
		//int sX = (width/2)*2;
		//int sY = (height/2)*2;
		int sX = (GH.MAZE_WIDTH/2)*2;
		int sY = (GH.MAZE_HEIGHT/2)*2;
		//set the probability to change path in the algorithm after advancing
		double prob;
		if (GH.USE_CUSTOM_PROB) 
			prob = GH.PRIM_PROB;
		else 
			prob = rnd.nextDouble();
		
		//print some info on the console
		System.out.println("Generando nuevo laberinto (" + this.width + "," + this.height + ")con semilla: " + seed);
		System.out.println("Probabilidad de parar: " + prob);
		
		//start generation
		generate(sX,sY,prob); 

		//if the process was ended by a kill command, show different data that the one shown when normally ended
		if (!this.KILL) {
			System.out.println("Colisiones: " + this.colisionesStatus);
			System.out.println("Tiempo: " + (System.currentTimeMillis() - time) + "ms");
		} else
			System.out.println("Generación terminada");
	}
	/**
	 * Actual generation algortithm
	 * @param x	starting generation x point
	 * @param y starting generation y point
	 * @param probability probability of the algorithm taking a random path on map increment
	 */
	private void generate(int x, int y, double probability) {
		generating = true;
		this.map[x][y] = true;
		
		points = new ArrayList<Point>();
		Point cPoint;
		int xPos = x;
		int yPos = y;
		
		//int dir = rnd.nextInt(4);
		points.add(new Point(x,y, rnd));
		int dir;
		
		int numTimesNoReturn = 0;
		
		while(true) {	
			//if the  kill flag was set, end generation
			if (this.KILL) {
				break;
			}
			//if paused, sleep for 50 msecs
			if (GH.PAUSE) {
				if (GH.STEP == 0) {
					try {
						//this way we only do 20 checks per second, enough to make it seem fluid without spending resources on a infinite loop
						Thread.sleep(50);
						this.startTimeGen += 50;
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					continue;
				} else {
					GH.STEP--;
				}

			}

			
			/**
			 * Take a point of the already created maze. Take a random direction. If the new point in that direction
			 * isn't visited yet, add it to the potential growing points. If it is visited, try with the next direction.
			 * If all directions of that point have already been visited, remove that point from the growing list.
			 * If the growing list is empty, generation terminates.
			 * 
			 * NOTE: there is a variable that controls random jumping between growing points. Whenever the number of
			 * new points visited without jumping exceeds said variable, a random jump is forced. this allows for mazes
			 * with longer pathways (jump when infinite) or with really sharp and small pathways (jump as soon as a new
			 * point is added)
			 */
			
			
			cPoint = points.get(points.size()-1);
			xPos = cPoint.getX();
			yPos = cPoint.getY();
			dir = cPoint.getDir();
			
			if (!this.nextPlace(xPos,yPos,dir)) {
				//System.out.println(numTimesNoReturn*probability);
				if (numTimesNoReturn * probability >= 1.0) {
					numTimesNoReturn = 0;
					randomizeArray(points, rnd);
				}
				else {
					this.setPath(xPos,yPos,dir);
					//if (nextX(xPos,dir) == 0 && nextY(yPos,dir) == 0)
						//return;
					if ((this.placeStackStatus++) > this.maxStackSize)
						this.maxStackSize = this.placeStackStatus;
					this.placedSquaresStatus++;
					points.add(new Point(nextX(xPos,dir),nextY(yPos,dir),rnd));
					numTimesNoReturn++;
					if (GH.MAZE_SLOWNESS != 0)
						try {
							Thread.sleep(GH.MAZE_SLOWNESS);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
				}
			}
			else {
				numTimesNoReturn = 0;
				if (cPoint.nextDir() == cPoint.getStartDir()) {
					this.placeStackStatus--;
					Point p = points.get(points.size()-1);
					points.remove(points.size()-1);
					int cont = 0;
					for (int k = 0; k < 4; k++) {
						if (nextSquare(p.getX(),p.getY(),k))
							cont++;
					}
					if (cont == 1)
						this.colisionesStatus++;
				if (points.size()== 0) 
					break; //end while
				//this.randomizeArray(points, rnd);
				}
			}
		}
		
		//set the generating flag to false in case the mazeContainer wants to know the current state of generation
		generating = false;
		this.startTimeGen = System.currentTimeMillis()-this.startTimeGen;
	}
	
	/**
	 * Puts a random element from the list in the front position
	 * @param a list to randomize
	 * @param rand random number generator to use when randomizing the list
	 */
	private void randomizeArray(ArrayList<Point> a, Random rand) {
		//a.set(a.size()-1, a.set(rand.nextInt(a.size()-1), a.get(a.size()-1)));
		int rnd = rand.nextInt(a.size()-1); //Antes era hasta a.size() por si da algún problema
		Point tmp = a.get(rnd);
		a.set(rnd, a.get(a.size()-1));
		a.set(a.size()-1,tmp);
	}
	/**
	 * Breaks the walls between nodes when expanding the maze
	 * @param x starting x coordinate
	 * @param y starting y coordinate
	 * @param dir direction in which the path is created
	 */
	private void setPath(int x, int y, int dir) {
		switch(dir) {
		case 0: {
			this.map[x][y-1] = true;
			this.map[x][y-2] = true;
			break;
		}
		case 1:{
			this.map[x+1][y] = true;
			this.map[x+2][y] = true;
			break;
		}
		case 2:{
			this.map[x][y+1] = true;
			this.map[x][y+2] = true;
			break;
		}
		case 3:{
			this.map[x-1][y] = true;
			this.map[x-2][y] = true;
			break;
		}
		}
	}
	/**
	 * Gets the next Y coordinate given a direction 
	 * @param y
	 * @param dir
	 * @return the new Y coordinate
	 */
	private int nextY(int y, int dir) {
		if (dir == 0)
			return y-2;
		if (dir == 2)
			return y+2;
		return y;
	}
	/**
	 * Gets the next X coordinate given a direction 
	 * @param x
	 * @param dir
	 * @return the new X coordinate
	 */
	private int nextX(int x, int dir) {
		if (dir == 1)
			return x+2;
		if (dir == 3)
			return x-2;
		return x;
	}
	/**
	 * Gets the "visited" value of the node we would encounter if travelling in direction dir. 
	 * @param x
	 * @param y
	 * @param dir 
	 * @return the "visited" value of the new place if it exists, true otherwise
	 */
	private boolean nextPlace(int x, int y, int dir) {
		switch(dir) {
		case 0: {
			if (y == 0)
				return true;
			return this.map[x][y-2];
		}
		case 1: {
			if (x >= this.width*2-2)
				return true;
			return this.map[x+2][y];
		}
		case 2: {
			if (y >= this.height*2-2)
				return true;
			return this.map[x][y+2];
		}
		case 3: {
			if (x == 0)
				return true;
			return this.map[x-2][y];
		}
		}
		return true;
	}
	/**
	 * Gets the value of the map cell (node or wall) we would encounter if travelling in direction dir
	 * @param x
	 * @param y
	 * @param dir
	 * @return same as nextPlace, but travelling in increments of 1
	 */
	private boolean nextSquare(int x, int y, int dir) {
		switch(dir) {
		case 0: {
			if (y == 0)
				return false;
			return this.map[x][y-1];
		}
		case 1: {
			if (x >= this.width*2-2)
				return false;
			return this.map[x+1][y];
		}
		case 2: {
			if (y >= this.height*2-2)
				return false;
			return this.map[x][y+1];
		}
		case 3: {
			if (x == 0)
				return false;
			return this.map[x-1][y];
		}
		}
		return true;
	}
	/**
	 * Returns the value of the map at a given coordinate
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean getSquare(int x, int y) {
		return this.map[x][y];
	}
	@SuppressWarnings("unused")
	/**
	 * Gets the number of collissions of the maze
	 * @return
	 */
	private int getColisiones() {
		int colisiones = 0;
		for (int i = 0; i < this.width; i++) 
			for (int j = 0; j < this.height; j++) {
				int cont = 0;
				for (int k = 0; k < 4; k++) {
					if (nextSquare(i*2,j*2,k))
						cont++;
				}
				if (cont == 1)
					colisiones++;
			}
		return colisiones;
	}
	/**
	 * 
	 * @return all the info of the maze
	 */
	public String getStatus() {
		return this.getStackStatus() + " " + this.getVisitedPlacesStatus() + " " + this.getPlacesLeftStatus() + " " + this.getElapsedTime() + " " + this.colisionesStatus;
	}
	public String getStackStatus() {
		return "Stack size: " + this.placeStackStatus + " (" + this.maxStackSize + ")";
	}
	public String getVisitedPlacesStatus() {
		return "Visited Places: " + this.placedSquaresStatus;
	}
	public String getPlacesLeftStatus() {
		return "Places Left: " + (this.width*this.height-this.placedSquaresStatus);
	}
	public String getElapsedTime() {
		long a;
		if (generating)
			a = System.currentTimeMillis()-this.startTimeGen;
		else
			a = this.startTimeGen;
		return "Time elapsed: " + (a/1000) + "," + (a-((a/1000)*1000));
	}
	public String getColisionesStatus() {
		return "Colissions: " + this.colisionesStatus;
	}
	public String getCurrentPointData() {
		try {
			Point p = this.points.get(this.points.size()-1);
			return "Position: (" + p.getX() + "," +p.getY() + ") " + p.getDir();
		} catch (Exception ex) {
			return "Position not found";
		}
	}
	/**
	 * 
	 * @return the map being used on the generation
	 */
	public boolean[][] getMap() {
		return this.map;
	}
	/**
	 * 
	 * @return the stack array being used in the generation
	 */
	public ArrayList<Point> getStack() {
		return this.points;
	}
	/**
	 * 
	 * @return true if the generation process is active, false otherwise
	 */
	public boolean isGenerating() {
		return this.generating;
	}
	
	@Override
	public void run() {

		Random r = new Random();
		generate(r.nextLong());
	
		//System.out.println("He acabado");
	}

	/**
	 * Sets the kill flag to true in order to exit in the next generation iteration
	 */
	public void end() {
		this.KILL = true;
	}

}
